04_DDD/04 -- Eventos de Dominio.md

Eventos de Dominio

Si los bounded contexts no pueden acceder directamente a los datos de otros BCs, ¿cómo se comunican? ¿Cómo sabe el módulo de descripciones que se ha creado una nueva variable, si no puede hacer SELECT * FROM variable WHERE...?

La respuesta, tal como hemos observado en el anterior capítulo, son los eventos de dominio: notificaciones inmutables que representan algo que ha ocurrido en el sistema. En lugar de que el módulo de descripciones consulte activamente datos de otro módulo, es el módulo de origen quien anuncia los cambios que le conciernen:

"Se ha creado una variable con id=42 y name='T_HORNO_PRINCIPAL'"

El módulo de descripciones escucha ese anuncio y reacciona en consecuencia — sin conocer los detalles internos del módulo emisor.


Qué es un evento de dominio

Un evento de dominio es un hecho que ha ocurrido en el sistema. Tres características lo definen:

  1. Inmutable — representa algo que ya pasó; no se modifica.
  2. Nombrado en pasadoGroupCreated, VariableChanged, QueryRemoved.
  3. Contiene primitivos — no Value Objects; facilita la serialización para transporte.
// GroupCreated.java — en el módulo de grupos (emisor)
public class GroupCreated extends DomainEvent {
    private final String name;

    public GroupCreated(final String aggregateId, final String name) {
        super(aggregateId); // el ID del grupo
        this.name = name;
    }

    public String getName() { return name; }

    @Override 
    public String eventName() { return "group.created"; }
}

El evento lleva solo lo que el receptor necesita: el id del grupo (aggregateId) y su nombre.


El flujo: publicar y escuchar

El flujo de un evento de dominio tiene dos actores: el módulo que origina el cambio y el módulo que reacciona. No hay acoplamiento directo entre ellos — solo el evento como contrato compartido.

El módulo emisor crea el evento y lo publica a través del bus:

// En el servicio del módulo de grupos
DomainEvent event = new GroupCreated(groupId, groupName);
eventBus.publish(event);

El módulo receptor declara un listener que reacciona cuando ese evento llega:

@Service
@DomainEventSubscriber(GroupCreated.class)
@RequiredArgsConstructor
public class OnGroupCreatedCreateDescriptionGroup {
    private final OnGroupCreator creator;

    @EventListener
    public void on(final GroupCreated event) {
        creator.create(Long.parseLong(event.aggregateId()), event.getName());
    }
}

@DomainEventSubscriber declara la suscripción al tipo de evento; @EventListener es la anotación de Spring que conecta el método con el bus interno. El módulo de grupos no sabe que existe este listener, y el módulo de descripciones no sabe nada del módulo de grupos — solo conoce la forma del evento.

El mismo patrón se replica para los tres eventos del ciclo de vida de cada tipo de entidad: Created, Changed y Removed. En total, el módulo de descripciones tiene veinticuatro listeners — tres por cada uno de los ocho tipos de recurso que gestiona.


Tell, Don't Ask: el evento como anuncio

Este patrón es la materialización entre bounded contexts del principio Tell, Don't Ask: en lugar de preguntar a un objeto por su estado y decidir tú qué hacer con esa información, dile lo que ha pasado y deja que él decida cómo reaccionar. La regla de negocio vive con quien tiene los datos para aplicarla, no con quien observa desde fuera.

Un evento de dominio es la expresión más pura del "tell". El emisor anuncia un hecho consumado, no consulta a nadie y no espera respuesta; el receptor decide qué hacer dentro de su propia lógica, con los datos que le llegan en el evento. El módulo de grupos no inspecciona al de descripciones, no le pide permiso, no comprueba si quiere recibir el aviso. El módulo de descripciones no le pide datos adicionales al emisor — todo lo que necesita está en el GroupCreated.

Junto con la Ley de Demeter aplicada a los BCs (capítulo anterior) — la primera restringe a quién se le puede preguntar, Tell, Don't Ask restringe quién decide — produce el rasgo central de la arquitectura DWall: módulos que cooperan sin conocerse.


El EventBus como Puerto

El EventBus sigue el patrón Ports and Adapters: es una interfaz definida en dominio, y su implementación concreta vive en infraestructura.

EventBus (interfaz — dominio)
    │
    ├── SpringEventBus     — usa ApplicationEventPublisher de Spring (in-process, síncrono)
    ├── RabbitMqEventBus   — envía a colas AMQP (asíncrono, distribuido)
    └── InMemoryEventBus   — para tests unitarios

En DWall, la implementación activa usa ApplicationEventPublisher de Spring para eventos in-process síncronos. Esto significa que cuando DescriptionService publica un GroupCreated, el listener del módulo de descripciones se ejecuta en la misma transacción, en el mismo hilo.


Resiliencia y consistencia

Esta arquitectura tiene una limitación inherente al uso de eventos Spring síncronos e in-process: no existe garantía de consistencia transaccional entre la entidad origen y su proyección en el módulo de descripciones (por ejemplo). Si la base de datos falla después de que el commit de la entidad origen se realice pero antes de que el listener termine de ejecutarse, el registro mirror y la descripción vacía nunca se crean correctamente.

DWall mitiga esto parcialmente mediante un mecanismo de resiliencia: el EventListenerHandleExceptionAspect intercepta mediante AOP cualquier excepción lanzada por un @EventListener y persiste el evento fallido en la tabla system_events_retry_domain_events. El JooqDomainEventConsumer la procesa en cada ciclo reintentando los eventos pendientes; si el reintento tiene éxito el registro se elimina, si vuelve a fallar permanece en la tabla como cola de eventos muertos disponible para inspección y reintento manual.

En la práctica, el sistema ofrece una fiabilidad muy alta: los escenarios en los que el mecanismo de retry no es suficiente son extraordinariamente excepcionales. Además, el bus de eventos es infraestructura propia de DWall, fuera del alcance de este TFG, por lo que se tomará como una garantía dada del sistema sobre la que construir.